// Copyright 2019 The Go Authors. All rights reserved.
// Use of this session code is governed by a BSD-style
// license that can be found in the LICENSE file.

package session

import (
	"context"
	"errors"
	"mi/compiler/ast"
	"mi/compiler/binder"
	"mi/compiler/checker"
	"mi/compiler/token"
	"mi/internal/span"
	"mi/internal/xpath"
)

const PlainText = "plaintext"
const Markdown = "markdown"

type MarkupContent struct {
	Kind string
	Value string
}

type HoverInfo struct {
	Contents MarkupContent
	Range span.UTF16Span
}

func (s *session) GetHoverInfo(_ context.Context, uri xpath.URI, pos span.UTF16Position) (*HoverInfo, error) {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	var sourceFile = s.program.GetSourceFile(uri)
	if sourceFile == nil {
		return nil, errors.New("Unknown file " + uri.Filename())
	}

	// Convert utf16 pos to byte offset
	offset, err := pos.ToOffsetWithCache(sourceFile.Text, sourceFile.GetLineStarts())
	if err != nil {
		return nil, err
	}

	var node = getTouchingName(sourceFile, offset, true)
	if node == sourceFile {
		return nil, nil
	}

	if isLabelName(node) {
		return nil, nil
	}

	var typeChecker = s.program.GetTypeChecker()
	var symbol = typeChecker.GetSymbolAtLocation(node)

	if symbol == nil || typeChecker.IsUnknownSymbol(symbol) {
		return nil, nil
	}

	if symbol.Flags&(binder.Variable|binder.FieldLike) != 0 {
		return s.getHoverInfoFromVariableLike(node, symbol, typeChecker)
	} else if isNameOfFunctionDeclaration(node) {
		return s.getHoverInfoFromFunctionName(node, symbol, typeChecker)
	} else if isCallExpressionTarget(node) {
		return s.getHoverInfoFromCallExpressionTarget(node, symbol, typeChecker)
	} else if isDocumentCallExpressionTarget(node) {
		return s.getHoverInfoFromCodeDocumentCallExpressionTarget(node, symbol, typeChecker)
	}

	var tpe = typeChecker.GetTypeOfNode(node)
	if tpe != nil {
		return &HoverInfo{
			Contents: MarkupContent{
				Kind:  PlainText,
				Value: typeChecker.TypeToString(tpe, nil, true),
			},
			Range: getNodeSpan(node),
		}, nil
	}

	return  nil, nil
}

func (s *session) getHoverInfoFromVariableLike(node ast.Node, symbol *binder.Symbol, checker checker.TypeChecker) (*HoverInfo, error) {
	var writer textWriter
	var tpe = checker.GetTypeOfSymbol(symbol)
	var typeString = checker.TypeToString(tpe, node, true)
	writer.writeCodeFunc("mi", func() {
		writer.writeString(token.VarKeyword.ToString())
		writer.writeString(" ")
		writer.writeString(symbol.Name)
		writer.writeString(": ")
		writer.writeString(typeString)
	})

	var content = documentConvertToMarkdown(symbol.ValueDeclaration.GetDocuments(), checker)
	if len(content) != 0{
		writer.writeString("---")
		writer.writeLine()
		writer.writeString(content)
	}

	return &HoverInfo{
		Contents: MarkupContent{
			Kind:  Markdown,
			Value: writer.string(),
		},
		Range: getNodeSpan(node),
	}, nil
}

func signatureToString(location ast.Node, name string, signature *checker.Signature, checker checker.TypeChecker, writer *textWriter)  {
	writer.writeString(token.FunctionKeyword.ToString())
	writer.writeString(" ")
	writer.writeString(name)
	if len(signature.TypeParameters) > 0 {
		writer.writeString("<")
		for i, parameter := range signature.TypeParameters {
			if i > 0 {
				writer.writeString(", ")
			}
			writer.writeString(checker.TypeToString(parameter, nil, true))
		}
		writer.writeString(">")
	}
	writer.writeString("(")
	for i, parameter := range signature.Parameters {
		if i > 0 {
			writer.writeString(", ")
		}

		var declaration = parameter.ValueDeclaration.(*ast.ParameterDeclaration)
		if !isNull(declaration.Dots) {
			writer.writeString("...")
			writer.writeString(" ")
		}
		writer.writeString(parameter.Name)
		writer.writeString(": ")
		var tpe = checker.GetTypeOfSymbol(parameter)
		writer.writeString(checker.TypeToString(tpe, location, true))
	}
	writer.writeString(")")

	if !isNull(signature.ResolvedReturnType) {
		writer.writeString(": ")
		writer.writeString(checker.TypeToString(signature.ResolvedReturnType, location, true))
	}

	if len(signature.Throws) > 0 {
		writer.writeString(" ")
		writer.writeString(token.ThrowsKeyword.ToString())
		writer.writeString(" ")
		for _, throw := range signature.Throws {
			writer.writeString(checker.TypeToString(throw, location, true))
		}
	}
}

func (s *session) getHoverInfoFromFunctionName(node ast.Node, symbol *binder.Symbol, checker checker.TypeChecker) (*HoverInfo, error) {
	var function = node.Parent().(ast.FunctionLike)
	var signature = checker.GetSignatureFromDeclaration(function)
	if isNull(signature) {
		return nil, nil
	}

	var name string
	if ident := ast.GetDeclarationName(function); !isNull(ident) {
		name = ident.Value
	} else {
		name = "?"
	}

	var writer textWriter
	writer.writeCodeFunc("mi", func() {
		signatureToString(node, name, signature, checker, &writer)
	})

	var content = documentConvertToMarkdown(node.Parent().GetDocuments(), checker)
	if len(content) != 0{
		writer.writeLine()
		writer.writeString("***")
		writer.writeLine()
		writer.writeString(content)
	}

	return &HoverInfo{
		Contents: MarkupContent{
			Kind:  Markdown,
			Value: writer.string(),
		},
		Range: getNodeSpan(node),
	}, nil
}

func (s *session) getHoverInfoFromCallExpressionTarget(node ast.Node, symbol *binder.Symbol, checker checker.TypeChecker) (*HoverInfo, error) {
	var name string
	if ast.IsIdentifier(node) {
		name = node.(*ast.Identifier).Value
	} else {
		name = "Missing"
	}

	var writer textWriter
	signature, candidates := checker.GetResolvedSignature(getContainCallExpression(node))
	if (signature == nil || checker.IsUnknownSignature(signature)) && len(candidates) > 0 {
		signature = candidates[0]
	}

	if signature != nil && !isNull(signature.Declaration) {
		writer.writeCodeFunc("mi", func() {
			signatureToString(node, name, signature, checker, &writer)
		})
		var content = documentConvertToMarkdown(signature.Declaration.GetDocuments(), checker)
		if len(content) != 0{
			writer.writeLine()
			writer.writeString("***")
			writer.writeLine()
			writer.writeString(content)
		}
	}

	return &HoverInfo{
		Contents: MarkupContent{
			Kind:  Markdown,
			Value: writer.string(),
		},
		Range: getNodeSpan(node),
	}, nil
}

func (s *session) getHoverInfoFromCodeDocumentCallExpressionTarget(node ast.Node, symbol *binder.Symbol, checker checker.TypeChecker) (*HoverInfo, error) {
	var name string
	if ast.IsIdentifier(node) {
		name = node.(*ast.Identifier).Value
	} else {
		name = "Missing"
	}

	var writer textWriter
	signature, candidates := checker.GetResolvedSignature(getContainDocumentCallExpression(node))
	if signature == nil || checker.IsUnknownSignature(signature) && len(candidates) > 0 {
		signature = candidates[0]
	}

	if signature != nil && !isNull(signature.Declaration) {
		writer.writeCodeFunc("mi", func() {
			signatureToString(node, name, signature, checker, &writer)
		})
		var content = documentConvertToMarkdown(signature.Declaration.GetDocuments(), checker)
		if len(content) != 0{
			writer.writeLine()
			writer.writeString("***")
			writer.writeLine()
			writer.writeString(content)
		}
	}

	return &HoverInfo{
		Contents: MarkupContent{
			Kind:  Markdown,
			Value: writer.string(),
		},
		Range: getNodeSpan(node),
	}, nil
}